/*
 * Copyright 2011 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package sweezy.swf.encoder
{
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	
	import sweezy.swf.abc.Opcode;
	import sweezy.swf.abc.Opcodes;
	import sweezy.swf.abc.ABC;
	import sweezy.swf.abc.structure.ClassInfo;
	import sweezy.swf.abc.structure.ClassTrait;
	import sweezy.swf.abc.structure.ConstTrait;
	import sweezy.swf.abc.structure.ConstantPoolInfo;
	import sweezy.swf.abc.structure.ExceptionInfo;
	import sweezy.swf.abc.structure.FunctionTrait;
	import sweezy.swf.abc.structure.GetterTrait;
	import sweezy.swf.abc.structure.InstanceInfo;
	import sweezy.swf.abc.structure.MetadataInfo;
	import sweezy.swf.abc.structure.MetadataItemInfo;
	import sweezy.swf.abc.structure.MethodBodyInfo;
	import sweezy.swf.abc.structure.MethodInfo;
	import sweezy.swf.abc.structure.MethodTrait;
	import sweezy.swf.abc.structure.Multiname;
	import sweezy.swf.abc.structure.MultinameA;
	import sweezy.swf.abc.structure.MultinameInfo;
	import sweezy.swf.abc.structure.MultinameL;
	import sweezy.swf.abc.structure.MultinameLA;
	import sweezy.swf.abc.structure.NameL;
	import sweezy.swf.abc.structure.NameLA;
	import sweezy.swf.abc.structure.NamespaceInfo;
	import sweezy.swf.abc.structure.NamespaceSetInfo;
	import sweezy.swf.abc.structure.OptionDetail;
	import sweezy.swf.abc.structure.QNameA;
	import sweezy.swf.abc.structure.QNameInfo;
	import sweezy.swf.abc.structure.RTQName;
	import sweezy.swf.abc.structure.RTQNameA;
	import sweezy.swf.abc.structure.RTQNameL;
	import sweezy.swf.abc.structure.RTQNameLA;
	import sweezy.swf.abc.structure.ScriptInfo;
	import sweezy.swf.abc.structure.SetterTrait;
	import sweezy.swf.abc.structure.SlotTrait;
	import sweezy.swf.abc.structure.TraitInfo;
	import sweezy.swf.abc.structure.TypeName;
	import sweezy.swf.constant.InstanceInfoFlags;
	import sweezy.swf.constant.MethodInfoFlags;
	import sweezy.swf.constant.TraitAttributes;
	import sweezy.swf.io.SWFOutput;
	import sweezy.swf.visitor.ABCVisitor;

	internal class ABCEncoderBase extends ABCVisitor
	{

		private var _out:SWFOutput;

		public function ABCEncoderBase(bytes:ByteArray)
		{
			_out = new SWFOutput(bytes);
		}

		override public function visitABC(element:ABC):void
		{
			_out.writeUI16(element.minorVersion);
			_out.writeUI16(element.majorVersion);

			element.constantPool.accept(this);

			_out.writeEncodedU32(element.methodInfos.length);
			for each (var methodInfo:MethodInfo in element.methodInfos)
			{
				methodInfo.accept(this);
			}

			_out.writeEncodedU32(element.metadataInfos.length);
			for each (var metadataInfo:MetadataInfo in element.metadataInfos)
			{
				metadataInfo.accept(this);
			}

			_out.writeEncodedU32(element.instanceInfos.length);
			for each (var instanceInfo:InstanceInfo in element.instanceInfos)
			{
				instanceInfo.accept(this);
			}

			for each (var classInfo:ClassInfo in element.classInfos)
			{
				classInfo.accept(this);
			}

			_out.writeEncodedU32(element.scriptInfos.length);
			for each (var scriptInfo:ScriptInfo in element.scriptInfos)
			{
				scriptInfo.accept(this);
			}

			_out.writeEncodedU32(element.methodBodyInfos.length);
			for each (var methodBodyInfo:MethodBodyInfo in element.methodBodyInfos)
			{
				methodBodyInfo.accept(this);
			}
		}

		override public function visitClassInfo(element:ClassInfo):void
		{
			_out.writeEncodedU32(element.cinit);
			_out.writeEncodedU32(element.traitInfos.length);
			for each (var traitInfo:TraitInfo in element.traitInfos)
			{
				traitInfo.accept(this);
			}
		}

		override public function visitClassTrait(element:ClassTrait):void
		{
			preEncodeTrait(element);

			_out.writeEncodedU32(element.slotId);
			_out.writeEncodedU32(element.classIndex);

			postEncodeTrait(element);
		}

		override public function visitConstTrait(element:ConstTrait):void
		{
			preEncodeTrait(element);

			_out.writeEncodedU32(element.slotId);
			_out.writeEncodedU32(element.typeName);
			_out.writeEncodedU32(element.vIndex);
			if (element.vIndex > 0)
			{
				_out.writeUI8(element.vKind);
			}

			postEncodeTrait(element);
		}

		override public function visitConstantPoolInfo(element:ConstantPoolInfo):void
		{
			var i:uint, len:uint, array:Array;

			array = element.integerConstants;
			len = array.length;
			_out.writeEncodedU32(len < 2 ? 0 : len);
			for (i = 1; i < len; i++)
			{
				_out.writeEncodedS32(array[i]);
			}

			array = element.unsignedIntegerConstants;
			len = array.length;
			_out.writeEncodedU32(len < 2 ? 0 : len);
			for (i = 1; i < len; i++)
			{
				_out.writeEncodedU32(array[i]);
			}

			array = element.doubleConstants;
			len = array.length;
			_out.writeEncodedU32(len < 2 ? 0 : len);
			for (i = 1; i < len; i++)
			{
				_out.writeDOUBLE(array[i]);
			}

			array = element.stringConstants;
			len = array.length;
			_out.writeEncodedU32(len < 2 ? 0 : len);
			var bytes:ByteArray = new ByteArray();
			for (i = 1; i < len; i++)
			{
				var str:String = element.stringConstants[i];
				if (str.length === 0)
				{
					_out.writeEncodedU32(0);
					continue;
				}

				bytes.writeUTFBytes(str);
				_out.writeEncodedU32(bytes.length);
				_out.writeBytes(bytes);
				bytes.clear();
			}

			array = element.namespaceInfos;
			len = array.length;
			_out.writeEncodedU32(len < 2 ? 0 : len);
			for (i = 1; i < len; i++)
			{
				var namespaceInfo:NamespaceInfo = array[i];
				namespaceInfo.accept(this);
			}

			array = element.namespaceSetInfos;
			len = array.length;
			_out.writeEncodedU32(len < 2 ? 0 : len);
			for (i = 1; i < len; i++)
			{
				var namespaceSetInfo:NamespaceSetInfo = array[i];
				namespaceSetInfo.accept(this);
			}

			array = element.multinameInfos;
			len = array.length;
			_out.writeEncodedU32(len < 2 ? 0 : len);
			for (i = 1; i < len; i++)
			{
				var multinametInfo:MultinameInfo = array[i];
				multinametInfo.accept(this);
			}
		}

		override public function visitExceptionInfo(element:ExceptionInfo):void
		{
			_out.writeEncodedU32(element.codeIndexFrom);
			_out.writeEncodedU32(element.codeIndexTo);
			_out.writeEncodedU32(element.target);
			_out.writeEncodedU32(element.exceptionType);
			_out.writeEncodedU32(element.varName);
		}

		override public function visitFunctionTrait(element:FunctionTrait):void
		{
			preEncodeTrait(element);

			_out.writeEncodedU32(element.slotId);
			_out.writeEncodedU32(element.functionIndex);

			postEncodeTrait(element);
		}

		override public function visitGetterTrait(element:GetterTrait):void
		{
			preEncodeTrait(element);

			_out.writeEncodedU32(element.dispId);
			_out.writeEncodedU32(element.method);

			postEncodeTrait(element);
		}

		override public function visitInstanceInfo(element:InstanceInfo):void
		{
			_out.writeEncodedU32(element.name);
			_out.writeEncodedU32(element.superName);
			_out.writeUI8(element.flags);

			if ((element.flags & InstanceInfoFlags.CLASS_PROTECTED_NS) === InstanceInfoFlags.CLASS_PROTECTED_NS)
			{
				_out.writeEncodedU32(element.protectedNs);
			}

			_out.writeEncodedU32(element.interfaces.length);
			for each (var interfaze:uint in element.interfaces)
			{
				_out.writeEncodedU32(interfaze);
			}

			_out.writeEncodedU32(element.iinit);

			_out.writeEncodedU32(element.traitInfos.length);
			for each (var traitInfo:TraitInfo in element.traitInfos)
			{
				traitInfo.accept(this);
			}
		}

		override public function visitMetadataInfo(element:MetadataInfo):void
		{
			_out.writeEncodedU32(element.name);

			var i:uint, len:uint = element.items.length, item:MetadataItemInfo;
			_out.writeEncodedU32(len);

			for (i = 0; i < len; i++)
			{
				item = element.items[i];
				_out.writeEncodedU32(item.key);
			}
			for (i = 0; i < len; i++)
			{
				item = element.items[i];
				_out.writeEncodedU32(item.val);
			}
		}

		override public function visitMethodBodyInfo(element:MethodBodyInfo):void
		{
			_out.writeEncodedU32(element.method);
			_out.writeEncodedU32(element.maxStack);
			_out.writeEncodedU32(element.localCount);
			_out.writeEncodedU32(element.initScopeDepth);
			_out.writeEncodedU32(element.maxScopeDepth);

			encodeOpcodes(element);

			_out.writeEncodedU32(element.exceptionInfos.length);
			for each (var exceptionInfo:ExceptionInfo in element.exceptionInfos)
			{
				exceptionInfo.accept(this);
			}

			_out.writeEncodedU32(element.traitInfos.length);
			for each (var traitInfo:TraitInfo in element.traitInfos)
			{
				traitInfo.accept(this);
			}
		}

		override public function visitMethodInfo(element:MethodInfo):void
		{
			_out.writeEncodedU32(element.parameterTypes.length);
			_out.writeEncodedU32(element.returnType);

			for each (var parameterType:uint in element.parameterTypes)
			{
				_out.writeEncodedU32(parameterType);
			}

			_out.writeEncodedU32(element.name);
			_out.writeUI8(element.flags);

			if ((element.flags & MethodInfoFlags.HAS_OPTIONAL) === MethodInfoFlags.HAS_OPTIONAL)
			{
				_out.writeEncodedU32(element.options.length);
				for each (var option:OptionDetail in element.options)
				{
					_out.writeEncodedU32(option.val);
					_out.writeUI8(option.kind);
				}
			}

			if ((element.flags & MethodInfoFlags.HAS_PARAM_NAMES) === MethodInfoFlags.HAS_PARAM_NAMES)
			{
				for each (var parameterName:uint in element.parameterNames)
				{
					_out.writeEncodedU32(parameterName);
				}
			}
		}

		override public function visitMethodTrait(element:MethodTrait):void
		{
			preEncodeTrait(element);

			_out.writeEncodedU32(element.dispId);
			_out.writeEncodedU32(element.method);

			postEncodeTrait(element);
		}

		override public function visitMultiname(element:Multiname):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.name);
			_out.writeEncodedU32(element.namespaceSet);
		}

		override public function visitMultinameA(element:MultinameA):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.name);
			_out.writeEncodedU32(element.namespaceSet);
		}

		override public function visitMultinameL(element:MultinameL):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.namespaceSet);
		}

		override public function visitMultinameLA(element:MultinameLA):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.namespaceSet);
		}

		override public function visitNameL(element:NameL):void
		{
			_out.writeUI8(element.kind);
		}

		override public function visitNameLA(element:NameLA):void
		{
			_out.writeUI8(element.kind);
		}

		override public function visitNamespaceInfo(element:NamespaceInfo):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.name);
		}

		override public function visitNamespaceSetInfo(element:NamespaceSetInfo):void
		{
			_out.writeEncodedU32(element.namespaceInfos.length);
			for each (var namespaceInfo:uint in element.namespaceInfos)
			{
				_out.writeEncodedU32(namespaceInfo);
			}
		}

		override public function visitQName(element:QNameInfo):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.ns);
			_out.writeEncodedU32(element.name);
		}

		override public function visitQNameA(element:QNameA):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.ns);
			_out.writeEncodedU32(element.name);
		}

		override public function visitRTQName(element:RTQName):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.name);
		}

		override public function visitRTQNameA(element:RTQNameA):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.name);
		}

		override public function visitRTQNameL(element:RTQNameL):void
		{
			_out.writeUI8(element.kind);
		}

		override public function visitRTQNameLA(element:RTQNameLA):void
		{
			_out.writeUI8(element.kind);
		}

		override public function visitScriptInfo(element:ScriptInfo):void
		{
			_out.writeEncodedU32(element.init);

			_out.writeEncodedU32(element.traitInfos.length);
			for each (var traitInfo:TraitInfo in element.traitInfos)
			{
				traitInfo.accept(this);
			}
		}

		override public function visitSetterTrait(element:SetterTrait):void
		{
			preEncodeTrait(element);

			_out.writeEncodedU32(element.dispId);
			_out.writeEncodedU32(element.method);

			postEncodeTrait(element);
		}

		override public function visitSlotTrait(element:SlotTrait):void
		{
			preEncodeTrait(element);

			_out.writeEncodedU32(element.slotId);
			_out.writeEncodedU32(element.typeName);
			_out.writeEncodedU32(element.vIndex);
			if (element.vIndex > 0)
			{
				_out.writeUI8(element.vKind);
			}

			postEncodeTrait(element);
		}

		override public function visitTypeName(element:TypeName):void
		{
			_out.writeUI8(element.kind);
			_out.writeEncodedU32(element.typeName);

			_out.writeEncodedU32(element.types.length);
			for each (var type:uint in element.types)
			{
				_out.writeEncodedU32(type);
			}
		}

		private function encodeOpcodes(element:MethodBodyInfo):void
		{
			var bytes:ByteArray = new ByteArray();
			bytes.endian = Endian.LITTLE_ENDIAN;
			_out.pushBytes(bytes);

			for each (var opcode:Opcode in element.opcodes)
			{
				_out.writeUI8(opcode.code);
				switch (opcode.code)
				{
					case Opcodes.OP_ifeq:
					case Opcodes.OP_iffalse:
					case Opcodes.OP_ifge:
					case Opcodes.OP_ifgt:
					case Opcodes.OP_ifle:
					case Opcodes.OP_iflt:
					case Opcodes.OP_ifne:
					case Opcodes.OP_ifnge:
					case Opcodes.OP_ifngt:
					case Opcodes.OP_ifnle:
					case Opcodes.OP_ifnlt:
					case Opcodes.OP_ifstricteq:
					case Opcodes.OP_ifstrictne:
					case Opcodes.OP_iftrue:
					case Opcodes.OP_jump:
						_out.writeSI24(opcode["offset"]);
						break;
					case Opcodes.OP_astype:
					case Opcodes.OP_coerce:
					case Opcodes.OP_debugfile:
					case Opcodes.OP_declocal:
					case Opcodes.OP_declocal_i:
					case Opcodes.OP_deleteproperty:
					case Opcodes.OP_dxns:
					case Opcodes.OP_finddef:
					case Opcodes.OP_findproperty:
					case Opcodes.OP_findpropstrict:
					case Opcodes.OP_getdescendants:
					case Opcodes.OP_getlex:
					case Opcodes.OP_getlocal:
					case Opcodes.OP_getproperty:
					case Opcodes.OP_getsuper:
					case Opcodes.OP_inclocal:
					case Opcodes.OP_inclocal_i:
					case Opcodes.OP_initproperty:
					case Opcodes.OP_istype:
					case Opcodes.OP_kill:
					case Opcodes.OP_newcatch:
					case Opcodes.OP_newclass:
					case Opcodes.OP_newfunction:
					case Opcodes.OP_pushdecimal:
					case Opcodes.OP_pushdouble:
					case Opcodes.OP_pushint:
					case Opcodes.OP_pushnamespace:
					case Opcodes.OP_pushstring:
					case Opcodes.OP_pushuint:
					case Opcodes.OP_pushuninitialized:
					case Opcodes.OP_setlocal:
					case Opcodes.OP_setproperty:
					case Opcodes.OP_setsuper:
						_out.writeEncodedU32(opcode["index"]);
						break;
					case Opcodes.OP_getscopeobject:
						_out.writeUI8(opcode["index"]);
						break;
					case Opcodes.OP_getglobalslot:
					case Opcodes.OP_getslot:
					case Opcodes.OP_setglobalslot:
					case Opcodes.OP_setslot:
						_out.writeEncodedU32(opcode["slotIndex"]);
						break;
					case Opcodes.OP_callstatic:
					case Opcodes.OP_callmethod:
					case Opcodes.OP_callproperty:
					case Opcodes.OP_callproplex:
					case Opcodes.OP_callsuper:
					case Opcodes.OP_callsupervoid:
					case Opcodes.OP_callpropvoid:
					case Opcodes.OP_constructprop:
						_out.writeEncodedU32(opcode["index"]);
						_out.writeEncodedU32(opcode["argCount"]);
						break;
					case Opcodes.OP_applytype:
					case Opcodes.OP_call:
					case Opcodes.OP_construct:
					case Opcodes.OP_constructsuper:
					case Opcodes.OP_newarray:
					case Opcodes.OP_newobject:
						_out.writeEncodedU32(opcode["argCount"]);
						break;
					case Opcodes.OP_add_p:
					case Opcodes.OP_convert_m_p:
					case Opcodes.OP_decrement_p:
					case Opcodes.OP_divide_p:
					case Opcodes.OP_increment_p:
					case Opcodes.OP_modulo_p:
					case Opcodes.OP_multiply_p:
					case Opcodes.OP_negate_p:
					case Opcodes.OP_subtract_p:
						_out.writeEncodedU32(opcode["param"]);
						break;
					case Opcodes.OP_declocal_p:
					case Opcodes.OP_inclocal_p:
						_out.writeEncodedU32(opcode["param"]);
						_out.writeEncodedU32(opcode["index"]);
						break;
					case Opcodes.OP_pushshort:
						_out.writeEncodedU32(opcode["shortValue"]);
						break;
					case Opcodes.OP_pushbyte:
						_out.writeUI8(opcode["byteValue"]);
						break;
					case Opcodes.OP_debugline:
						_out.writeEncodedU32(opcode["lineNumber"]);
						break;
					case Opcodes.OP_debug:
						_out.writeUI8(opcode["debugType"]);
						_out.writeEncodedU32(opcode["index"]);
						_out.writeUI8(opcode["register"]);
						_out.writeEncodedU32(opcode["extra"]);
						break;
					case Opcodes.OP_hasnext2:
						_out.writeEncodedU32(opcode["objectRegister"]);
						_out.writeEncodedU32(opcode["indexRegister"]);
						break;
					case Opcodes.OP_lookupswitch:
						_out.writeSI24(opcode["defaultOffset"]);
						var offsets:Array = opcode["caseOffsets"];
						_out.writeEncodedU32(offsets.length - 1);
						for each (var caseOffset:int in offsets)
						{
							_out.writeSI24(caseOffset);
						}
						break;
				}
			}

			_out.popBytes();
			_out.writeEncodedU32(bytes.length);
			_out.writeBytes(bytes);
		}

		private function postEncodeTrait(element:TraitInfo):void
		{
			if ((element.attributes & TraitAttributes.METADATA) === TraitAttributes.METADATA)
			{
				_out.writeEncodedU32(element.metadata.length);
				for each (var metadata:uint in element.metadata)
				{
					_out.writeEncodedU32(metadata);
				}
			}
		}

		private function preEncodeTrait(element:TraitInfo):void
		{
			_out.writeEncodedU32(element.name);
			_out.writeUB(4, element.attributes);
			_out.writeUB(4, element.traitType);
		}
	}
}
